﻿using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Diagnostics.Contracts;

using E = System.Linq.Expressions.Expression;

namespace ReactiveCode.Observables.Expressions
{
    using Util;

    static class ObservableExpressionExtensions
    {
        static Dictionary<int, Type> _tupleTypes = typeof(Tuple).Assembly.GetTypes()
            .Where(t => t.FullName.StartsWith("System.Tuple`"))
            .ToDictionary(t => t.GetGenericArguments().Length);

        static Type GetTupleType(params Type[] itemTypes)
        {
            return _tupleTypes[itemTypes.Length].MakeGenericType(itemTypes);
        }
        static Expression TupleCreate(params Expression[] items)
        {
            var itemTypes = items.Select(i => i.Type).ToArray();
            return E.New(_tupleTypes[items.Length].MakeGenericType(itemTypes).GetConstructor(itemTypes), items);
        }
        public static LambdaExpression CreateTuple(Type[] paramTypes, Type[] itemTypes)
        {
            var parameters = paramTypes.Select(t => E.Parameter(t)).ToArray();
            var items = parameters.Select((p, i) => p.Type == itemTypes[i] ? p as Expression : E.Convert(p, itemTypes[i])).ToArray();
            return E.Lambda(TupleCreate(items), parameters);
        }
        public static LambdaExpression CreateTuple(params Type[] paramTypes)
        {
            return CreateTuple(paramTypes, paramTypes);
        }
        static Expression AddItemToTuple(Expression tuple, Expression item)
        {
            var items = new List<Expression>();
            if (tuple.Type.Assembly == typeof(Tuple).Assembly && tuple.Type.FullName.StartsWith(typeof(Tuple).FullName))
            {
                int itemCount = tuple.Type.GetGenericArguments().Length;

                // todo: max 7 item support
                items.AddRange(Enumerable.Range(1, itemCount).Select(i => E.Property(tuple, "Item" + i) as Expression));
            }
            else
                items.Add(tuple);

            items.Add(item);
            return TupleCreate(items.ToArray());

        }

        [Pure]
        public static Type GetElementType(this Expression observable)
        {
            Contract.Requires(observable != null);
            return (from iface in Enumerable.Repeat(observable.Type, 1).Concat(observable.Type.GetInterfaces())
                    where iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IObservable<>)
                    select iface.GetGenericArguments().Single())
                    .Single();
        }


        static Expression Op(Type containerType, string name, Type[] typeArgs, params Expression[] args)
        {
            return Expression.Call(containerType, name, typeArgs, args);
        }
        static Expression Op(string name, Type[] typeArgs, params Expression[] args)
        {
            return Op(typeof(Observable), name, typeArgs, args);
        }

        public static Expression CombineLatest(this Expression left, Expression right, LambdaExpression resultSelector)
        {
            return CombineLatest(left, right, resultSelector, resultSelector.Body.Type);
        }
        public static Expression CombineLatest(this Expression left, Expression right, Expression resultSelector, Type resultType)
        {
            return Op("CombineLatest", new[] { left.GetElementType(), right.GetElementType(), resultType }, left, right, resultSelector);
        }
        public static Expression CombineLatest(this Expression left, Expression right, bool mergeTuples = false)
        {
            if (left == null)
                return right;
            else if (right == null)
                return left;
            else
            {
                var item1 = E.Parameter(left.GetElementType());
                var item2 = E.Parameter(right.GetElementType());
                LambdaExpression selector = mergeTuples ? E.Lambda(AddItemToTuple(item1, item2), item1, item2) : CreateTuple(item1.Type, item2.Type);
                return CombineLatest(left, right, selector);
            }
        }

        public static Expression CombineLatest(this IEnumerable<Expression> sources, Func<Expression[], Expression> resultSelector = null)
        {
            Contract.Assert(sources != null && sources.Any() && !sources.Contains(null));
            sources = sources.ToList();
            Expression merged = sources.Aggregate<E, E>(null, (a, b) => CombineLatest(a, b, true));
            Contract.Assert(merged != null);

            if (resultSelector == null)
                return merged;

            var p = Expression.Parameter(merged.GetElementType());
            var items = sources.Count() == 1
                ? new [] { p as Expression }
                : Enumerable.Range(1, sources.Count()).Select(i => E.Property(p, "Item" + i) as Expression).ToArray();

            var result = E.Lambda(resultSelector(items), p);
            return merged.Select(result);
        }
        public static Expression CombineLatestToArray(this IEnumerable<Expression> sources, Type elemType)
        {
            Contract.Assert(sources != null && sources.All(s => s.GetElementType() == elemType));

            var sourceArray = E.NewArrayInit(typeof(IObservable<>).MakeGenericType(elemType), sources);
            return Op(typeof(Extensions), "CombineLatest", new[] { elemType }, sourceArray);
        }

        public static Expression Select(this Expression source, LambdaExpression resultSelector)
        {
            return Op("Select", new[] { source.GetElementType(), resultSelector.Body.Type }, source, resultSelector);
        }
        public static Expression Where(this Expression source, LambdaExpression predicate)
        {
            return Op("Where", new[] { source.GetElementType() }, source, predicate);
        }

        public static Expression SelectMany(this Expression source, LambdaExpression resultSelector)
        {
            return Op("SelectMany", new[] { source.GetElementType(), resultSelector.Body.GetElementType() }, source, resultSelector);
        }
        public static Expression DistinctUntilChanged(this Expression source)
        {
            return Op("DistinctUntilChanged", new[] { source.GetElementType() }, source);
        }
        public static Expression Return(Expression value)
        {
            return Op("Return", new[] { value.Type }, value);
        }
        public static Expression IIf(this Expression test, Expression ifTrue, Expression ifFalse)
        {
            Contract.Assert(test != null && ifTrue != null && ifFalse != null);
            Contract.Assert(test.GetElementType() == typeof(bool) && ifTrue.GetElementType() == ifFalse.GetElementType());
            return Op(typeof(Extensions), "IIf", new[] { ifTrue.GetElementType() }, test, ifTrue, ifFalse);
        }
        public static Expression Cast(this Expression source, Type resultType)
        {
            Contract.Requires(source != null);
            var item = E.Parameter(source.GetElementType(), "x");
            var lambda = E.Lambda(E.Convert(item, resultType), item);
            return source.Select(lambda);
        }

        public static Expression ObserveOn(this Expression source, Expression scheduler)
        {
            return Op("ObserveOn", new[] { source.GetElementType() }, source, scheduler);
        }

        public static Expression PropertyOrField(this Expression source, MemberInfo member)
        {
            var item = E.Parameter(source.GetElementType(), "x");
            var memExpr = E.Lambda(E.MakeMemberAccess(item, member), item);
            return Op(typeof(Extensions), "PropertyOrField", new[] { ReflectionUtil.GetValueType(member) }, E.Quote(memExpr));
        }
    }
}
